/*
 * Copyright (C) 2015-present, Wei Chou (weichou2010@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.wei.c.widget;

import java.util.LinkedList;
import java.util.List;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;

import com.wei.c.lib.R;
import com.wei.c.utils.UIUtils;

/**
 * 天气曲线组件。
 * 可设置纵横比，并可以设置是基于width来算height，还是基于height来算width.
 * 
 * @author 周伟 Wei Chou(weichou2010@gmail.com)
 */
public class WeatherCurveView extends View {
    private static final boolean DEBUG          = false;

    public static final int BASE_ON_WIDTH       = 0;
    public static final int BASE_ON_HEIGHT      = 1;

    private static final DayTempers[] EMPTY_DATA = new DayTempers[0];

    private float mAspectRadio;
    private int mAspectBaseOn = BASE_ON_HEIGHT;

    private final List<Curve> mCurveList = new LinkedList<Curve>();
    private final Rect mContentBounds = new Rect(), mCurveBounds = new Rect();
    private final Paint mPaintText = new Paint();
    private final Paint mPaintCircle = new Paint();
    private int mTextSize, mTextCurveGap;
    private int mStartTime, mDuration;
    private float mDurationReciprocal;
    private boolean mComplete = false, mNeedPrepare = true, mAutoRestart = false;
    private int mRestartInterval = 1000;
    private int mCircleRadius;
    private DayTempers[] mData = EMPTY_DATA;
    private int mDrawCurvePointIndex = 0;

    public WeatherCurveView(Context context) {
        super(context);
        init();
    }

    public WeatherCurveView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WeatherCurveView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WeatherCurveView, defStyle, 0);
        setAspectRadio(a.getFloat(R.styleable.WeatherCurveView_wAspectRadio, 0));
        setAspectBaseOn(a.getInt(R.styleable.WeatherCurveView_wAspectBaseOn, BASE_ON_HEIGHT));
        setTextSize(a.getDimensionPixelSize(R.styleable.WeatherCurveView_textSize, UIUtils.dip2pix(getContext(), 12)));
        setTextCurveGap(a.getDimensionPixelSize(R.styleable.WeatherCurveView_textCurveGap, UIUtils.dip2pix(getContext(), 5)));
        setCircleRadius(a.getDimensionPixelSize(R.styleable.WeatherCurveView_circleRadius, UIUtils.dip2pix(getContext(), 2)));
        a.recycle();

        init();
    }

    private void init() {
        setWillNotDraw(false);
        setDuration(2000);

        mPaintCircle.setStyle(Paint.Style.STROKE);
        mPaintCircle.setAntiAlias(true);
        mPaintCircle.setFilterBitmap(true);
        mPaintCircle.setDither(true);
        mPaintCircle.setStrokeWidth(UIUtils.dip2pix(getContext(), 1.5f));

        mPaintText.setStyle(Paint.Style.STROKE);
        mPaintText.setAntiAlias(true);
        mPaintText.setFilterBitmap(true);
        mPaintText.setDither(true);

        /*15,-15
        25,0
        20,5
        -5,-15
        mData = new DayTempers[4];
        mData[0] = new DayTempers(15, -15, true);
        mData[1] = new DayTempers(25, 0, true);
        mData[2] = new DayTempers(20, 5, false);
        mData[3] = new DayTempers(-5, -15, false);*/
    }

    private int getColor(DayTempers dt) {
        return dt.dayPass ? 0x7fffffff : 0xffffffff;
    }

    private int getColor(DayTempers dt, boolean high) {
        return getColor(dt);    //high ? dt.dayPass ? 0x7fff0000 : 0xffff0000 : dt.dayPass ? 0x7f00ffff : 0xff00ffff;
    }

    public void setAspectRadio(float aspectRadio) {
        if (aspectRadio != mAspectRadio) {
            mAspectRadio = aspectRadio;
            requestLayout();
        }
    }

    public void setAspectBaseOn(int aspectBaseOn) {
        if (aspectBaseOn != mAspectBaseOn) {
            mAspectBaseOn = aspectBaseOn;
            requestLayout();
        }
    }

    public void setTextSizeDp(int dp) {
        setTextSize(UIUtils.dip2pix(getContext(), dp));
    }

    public void setTextSizeSp(int sp) {
        setTextSize(UIUtils.dip2pix(getContext(), sp));
    }

    private void setTextSize(int pixel) {
        if (pixel != mTextSize) {
            mTextSize = pixel;
            mPaintText.setTextSize(mTextSize);
            requestLayout();
        }
    }

    private void setTextCurveGap(int gap) {
        if (gap != mTextCurveGap) {
            mTextCurveGap = gap;
            requestLayout();
        }
    }

    private void setCircleRadius(int radius) {
        if (radius != mCircleRadius) {
            mCircleRadius = radius;
            requestLayout();
        }
    }

    public void setDataSource(List<TemperInfo> data, int todayIndex, boolean anim) {
        mData = infoToDayTempers(data, todayIndex);
        updateCurveBounds();
        startDrawCurves(anim);
    }

    /**一天的温度信息**/
    private static class DayTempers {
        /**最高温度**/
        public final int high;
        /**最低温度**/
        public final int low;
        /**该日期是以前还是今天之后。true表示以前，false表示今天以后（包括今天）**/
        public final boolean dayPass;
        /**基于View坐标的最高温度和最低温度所在的点**/
        private final Point pointH = new Point();
        private final Point pointL = new Point();

        public DayTempers(int h, int l, boolean pass) {
            high = h;
            low = l;
            dayPass = pass;
        }

        private int high(int h) {
            return Math.max(h, high);
        }

        private int low(int l) {
            return Math.min(l, low);
        }

        private void point(int curveHeight, int baseT, float f) {
            pointH.y = curveHeight - (int)((high - baseT) * f);
            pointL.y = curveHeight - (int)((low - baseT) * f);
        }
    }

    private static DayTempers[] infoToDayTempers(List<TemperInfo> infos, int todayIndex) {
        if (infos == null || infos.size() == 0) return EMPTY_DATA;
        DayTempers[] tes = new DayTempers[infos.size()];
        int i = 0;
        for (TemperInfo info : infos) {
            tes[i] = new DayTempers(info.getHigh(), info.getLow(), i < todayIndex);
            i++;
        }
        return tes;
    }

    public static interface TemperInfo {
        int getHigh();
        int getLow();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mAspectRadio > 0) {
            if (mAspectBaseOn == BASE_ON_HEIGHT) {
                super.onMeasure(MeasureSpec.makeMeasureSpec(
                        (int)(MeasureSpec.getSize(heightMeasureSpec) * mAspectRadio),
                        MeasureSpec.getMode(heightMeasureSpec)),
                        heightMeasureSpec);
            } else {
                super.onMeasure(widthMeasureSpec,
                        MeasureSpec.makeMeasureSpec(
                                (int)(MeasureSpec.getSize(widthMeasureSpec) * mAspectRadio),
                                MeasureSpec.getMode(widthMeasureSpec)));
            }
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        int pl = getPaddingLeft();
        int pt = getPaddingTop();
        int pr = getPaddingRight();
        int pb = getPaddingBottom();
        mContentBounds.set(pl, pt, getWidth() - pr, getHeight() - pb);

        updateCurveBounds();
        startDrawCurves(false);
    }

    private void updateCurveBounds() {
        if (mData != null && mData.length > 0 && mContentBounds.width() > 0 && mContentBounds.height() > 0) {
            int colume = mData.length;
            int columeWidth = mContentBounds.width() / colume;
            int halfCW = columeWidth / 2;
            if (DEBUG) Log.d("WeatherCurveView", mContentBounds.toShortString());
            mCurveBounds.set(mContentBounds.left + halfCW, mContentBounds.top + mTextSize + mTextCurveGap,
                    mContentBounds.right - halfCW, mContentBounds.bottom - mTextSize - mTextCurveGap);
            if (DEBUG) Log.d("WeatherCurveView", mCurveBounds.toShortString());
            //计算横坐标
            for (int i = 0; i < mData.length; i++) {
                mData[i].pointL.x = mData[i].pointH.x = (i == mData.length - 1) ? mCurveBounds.right : mCurveBounds.left + columeWidth * i;
                if (DEBUG) Log.d("WeatherCurveView", "mData[" + i + "].pointH.x==pointL.x=:" + mData[i].pointH.x);
            }
            //计算纵坐标
            int maxT = Integer.MIN_VALUE, minT = Integer.MAX_VALUE;
            for (DayTempers dt : mData) {
                maxT = dt.high(maxT);
                minT = dt.low(minT);
            }
            float f = mCurveBounds.height() * 1f / (maxT - minT);
            for (DayTempers dt : mData) {
                dt.point(mCurveBounds.bottom, minT, f);
                if (DEBUG) Log.d("WeatherCurveView", "dt.pointH.y----:" + dt.pointH.y);
                if (DEBUG) Log.d("WeatherCurveView", "dt.pointL.y----:" + dt.pointL.y);
            }
            resetCurveList();
        }
    }

    private void resetCurveList() {
        mCurveList.clear();
        if (mData != null && mData.length > 0) {
            for (int i = 1; i < mData.length; i++) {
                mCurveList.add(new DayWeatherCurve(getContext(), new Point(mData[i-1].pointH.x + mCircleRadius, mData[i-1].pointH.y),
                        new Point(mData[i].pointH.x - mCircleRadius, mData[i].pointH.y), getColor(mData[i-1], true), UIUtils.dip2pix(getContext(), 1.5f)));
                mCurveList.add(new DayWeatherCurve(getContext(), new Point(mData[i-1].pointL.x + mCircleRadius, mData[i-1].pointL.y),
                        new Point(mData[i].pointL.x - mCircleRadius, mData[i].pointL.y), getColor(mData[i-1], false), UIUtils.dip2pix(getContext(), 1.5f)));
            }
            makeCurvesPathes();
        }
    }

    private void makeCurvesPathes() {
        //doCurvesPrepare();
        //doCurvesPassTime(1);
        //由computeScroll()的动画来执行
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (!mComplete) {
            if (mNeedPrepare) {
                onPrepare();
                mNeedPrepare = false;
            }
            if (mStartTime == 0) {
                mStartTime = (int)AnimationUtils.currentAnimationTimeMillis();
                invalidate();
            } else {
                int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
                float timePercent = timePassed * mDurationReciprocal;
                boolean done = false;
                if (timePercent <= 1) {
                    onPassTime(timePercent);
                    invalidate();
                    done = true;
                }
                if (timePercent >= 1) {
                    if (!done) {
                        onPassTime(1);
                        invalidate();
                    }
                    onComplete();
                    mComplete = true;
                }
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        doCurvesDraw(canvas);
        drawCirclePoint(canvas, mDrawCurvePointIndex);
        drawText(canvas, mDrawCurvePointIndex);
    }

    private void drawCirclePoint(Canvas canvas, int index) {
        if (index >= mData.length) index = mData.length - 1;
        for (int i = 0; i <= index; i++) {
            DayTempers dt = mData[i];
            mPaintCircle.setColor(getColor(dt, true));
            canvas.drawCircle(dt.pointH.x, dt.pointH.y, mCircleRadius, mPaintCircle);
            mPaintCircle.setColor(getColor(dt, false));
            canvas.drawCircle(dt.pointL.x, dt.pointL.y, mCircleRadius, mPaintCircle);
        }
    }

    private void drawText(Canvas canvas, int index) {
        FontMetrics fm = mPaintText.getFontMetrics();
        int gap = (int)Math.abs((fm.bottom - fm.top) / 5);    //- Math.abs(fm.leading),
        if (index >= mData.length) index = mData.length - 1;
        for (int i = 0; i <= index; i++) {
            DayTempers dt = mData[i];
            mPaintText.setColor(getColor(dt));
            String texth = dt.high + "°";
            String textl = dt.low + "°";
            canvas.drawText(texth, dt.pointH.x - mPaintText.measureText(texth) * 2f / 5, dt.pointH.y - mTextCurveGap, mPaintText);
            canvas.drawText(textl, dt.pointL.x - mPaintText.measureText(textl) * 2f / 5, dt.pointL.y + mTextSize + mTextCurveGap - gap, mPaintText);
        }
    }

    public void startDrawCurves(boolean anim) {
        if (anim) {
            if (mComplete || mStartTime == 1) mStartTime = 0;
        } else if (mStartTime == 0) {
            mStartTime = 1;
        }
        mNeedPrepare = true;
        mComplete = false;
        invalidate();
    }

    public void stopCurvesAnimation() {
        if (!mComplete) mStartTime = 1;
    }

    public void setDuration(int duration) {
        mDuration = duration;
        mDurationReciprocal = 1.0f / mDuration;
    }

    protected void onPrepare() {
        doCurvesPrepare();
        mDrawCurvePointIndex = 0;
    }

    protected void onPassTime(float timePercent) {
        mDrawCurvePointIndex = doCurvesPassTime(timePercent);
    }

    protected void onComplete() {
        doCurvesComplete();
        if (mAutoRestart) postDelayed(new Runnable() {

            @Override
            public void run() {
                startDrawCurves(true);
            }
        }, mRestartInterval);
    }

    private void doCurvesPrepare() {
        for (Curve curve : mCurveList) {
            curve.prepare();
        }
    }

    /**@return 返回已经到达哪一个point**/
    private int doCurvesPassTime(float timePassedPercent) {
        int days = mCurveList.size() / 2;
        float dayPassed = 1f / days;
        boolean fill = timePassedPercent >= 1;
        int index = fill ? days : 0;
        for (int i = days - 1; i >= 0; --i) {
            if (fill) {
                mCurveList.get(i * 2).passTime(1);
                mCurveList.get(i * 2 + 1).passTime(1);
            } else {
                float fpass = dayPassed * i;
                if (timePassedPercent > fpass) {
                    fpass = (timePassedPercent - fpass) / dayPassed;
                    mCurveList.get(i * 2).passTime(fpass);
                    mCurveList.get(i * 2 + 1).passTime(fpass);
                    fill = true;
                    index = i;
                } else if (timePassedPercent == fpass) {
                    fill = true;
                    index = i;
                }
            }
        }
        return index;
    }

    private void doCurvesDraw(Canvas canvas) {
        for (Curve curve : mCurveList) {
            curve.draw(canvas);
        }
    }

    private void doCurvesComplete() {}

    private static class Curve {
        private final Path mPath = new Path();
        private final Paint mPaint;
        private final Interpolator mInterpolator;
        private final Point mPStart, mPEnd;
        private final int mStrokeWidth;
        private float mLastPassedPercent, mIncreaseUnit;
        private boolean mPathStarted = false;

        public Curve(Context context, Interpolator interpolator, Point pstart, Point pend, int color, int strokeWidth) {
            mInterpolator = interpolator;
            mPStart = pstart;
            mPEnd = pend;
            mStrokeWidth = strokeWidth;

            mPaint = new Paint();
            mPaint.setStyle(Paint.Style.STROKE);
            //设置抗锯齿，三者必须同时设置效果才可以
            mPaint.setAntiAlias(true);	//等同于mPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
            mPaint.setFilterBitmap(true);
            mPaint.setDither(true);
            mPaint.setColor(color);
            mPaint.setStrokeWidth(strokeWidth);
        }

        public void passTime(float timePassedPercent) {
            float timePassed = mLastPassedPercent + mIncreaseUnit;
            if (mLastPassedPercent < 1 && timePassed > 1) timePassed = 1;
            for (; timePassed <= timePassedPercent; timePassed += mIncreaseUnit) {
                if (!mPathStarted) {
                    mPath.moveTo(mPStart.x, mPStart.y);
                    mPathStarted = true;
                }
                mPath.lineTo(mPStart.x + (mPEnd.x - mPStart.x) * timePassed, mPStart.y + (mPEnd.y - mPStart.y) * mInterpolator.getInterpolation(timePassed));
            }
            mLastPassedPercent = timePassedPercent;
        }

        public void draw(Canvas canvas) {
            canvas.drawPath(mPath, mPaint);
        }

        public void prepare() {
            mPath.reset();
            mPathStarted = false;
            mLastPassedPercent = 0;
            if (mPStart != null && mPEnd != null && mPEnd.x - mPStart.x != 0) {
                mIncreaseUnit = mStrokeWidth * 1f / (mPEnd.x - mPStart.x);
            } else {
                throw new IllegalArgumentException();
            }
            onPrepared(mPath);
        }

        protected void onPrepared(Path path) {}
    }

    private static class DayWeatherCurve extends Curve {
        public DayWeatherCurve(Context context, Point pstart, Point pend, int color, int strokeWidth) {
            super(context, new AccelerateDecelerateInterpolator(), pstart, pend, color, strokeWidth);
        }
    }
}
